package edu.ntu.cloudroid.xacml.builder.policy;

import java.io.ByteArrayOutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;


import com.google.appengine.api.datastore.KeyFactory;
import com.sun.xacml.Indenter;
import com.sun.xacml.Obligation;
import com.sun.xacml.Policy;
import com.sun.xacml.Rule;
import com.sun.xacml.Target;
import com.sun.xacml.TargetMatch;
import com.sun.xacml.UnknownIdentifierException;
import com.sun.xacml.attr.AttributeDesignator;
import com.sun.xacml.attr.AttributeValue;
import com.sun.xacml.attr.BooleanAttribute;
import com.sun.xacml.attr.DoubleAttribute;
import com.sun.xacml.attr.IntegerAttribute;
import com.sun.xacml.attr.StringAttribute;
import com.sun.xacml.attr.TimeAttribute;
import com.sun.xacml.combine.CombiningAlgFactory;
import com.sun.xacml.combine.OrderedDenyOverridesRuleAlg;
import com.sun.xacml.combine.RuleCombiningAlgorithm;
import com.sun.xacml.cond.Apply;
import com.sun.xacml.cond.EqualFunction;
import com.sun.xacml.cond.Evaluatable;
import com.sun.xacml.cond.Function;
import com.sun.xacml.cond.FunctionFactory;
import com.sun.xacml.cond.FunctionTypeException;
import com.sun.xacml.cond.LogicalFunction;
import com.sun.xacml.ctx.Attribute;
import com.sun.xacml.ctx.Result;

import edu.ntu.cloudroid.model.IDs;
import edu.ntu.cloudroid.model.friend.GroupData;
import edu.ntu.cloudroid.model.friend.GroupInfo;
import edu.ntu.cloudroid.model.policy.AbstractPrivacyPreference;
import edu.ntu.cloudroid.model.policy.SimpleFiltering;
import edu.ntu.cloudroid.model.profile.ProfileData;
import edu.ntu.cloudroid.server.backendservice.PolicyService;
import edu.ntu.cloudroid.xacml.Utilities;
import edu.ntu.cloudroid.xacml.XACMLException;
import edu.ntu.cloudroid.xacml.XACMLStrings;

/**
 * Class to build a policy based on a <code>Person</code> or <code>Group</code>.
 * 
 * @author Milind Ganjoo
 * 
 */
public class AbstractPolicyBuilder {
	private static final Logger logger = Logger
			.getLogger(AbstractPolicyBuilder.class.getName());

	boolean groupSpecific=false;
	ProfileData requestedResource;
	AbstractPrivacyPreference preference;
	String groupIdString;
	String policyIdString;
	String groupName; 
	
	public AbstractPolicyBuilder(){
		
	}
			
	public void init(ProfileData p, AbstractPrivacyPreference preference) {
		this.requestedResource = p;
		this.preference = preference;
		policyIdString = "policy-" + KeyFactory.keyToString(p.getKey());
	}

	public void init(ProfileData resource, GroupData g) throws Exception{
		groupSpecific = true;
		this.requestedResource = resource;
		preference = PolicyService.getGroupPolicy(g);		
		this.groupName = g.getGroupName();
		
		groupIdString = KeyFactory.keyToString(g.getKey());
		policyIdString = "policy-"
				+ KeyFactory.keyToString(requestedResource.getKey())
				+ "-group-" + groupIdString;
	}
	

	/**
	 * Simple helper routine that creates a TargetMatch instance.
	 * 
	 * @param type
	 *            the type of match
	 * @param functionId
	 *            the matching function identifier
	 * @param designator
	 *            the AttributeDesignator used in this match
	 * @param value
	 *            the AttributeValue used in this match
	 * 
	 * @return the matching element
	 */
	protected TargetMatch createTargetMatch(int type, String functionId,
			AttributeDesignator designator, AttributeValue value) {
		try {
			// get the factory that handles Target functions and get an
			// instance of the right function
			FunctionFactory factory = FunctionFactory.getTargetInstance();
			Function function = factory.createFunction(functionId);

			// create the TargetMatch
			return new TargetMatch(type, function, designator, value);
		} catch (Exception e) {
			// note that in this example, we should never hit this case, but
			// in the real world you need to worry about exceptions, especially
			// from the factory
			return null;
		}
	}

	/**
	 * Creates the Target used in the Policy.
	 * 
	 * @return the target
	 * 
	 * @throws URISyntaxException
	 *             if there is a problem with any of the URIs
	 */
	protected Target createPolicyTarget() throws URISyntaxException {
		List<List<TargetMatch>> subjects = null;
		List<List<TargetMatch>> resources = new ArrayList<List<TargetMatch>>();

		// create the Subject section if we are being group-specific
		if (groupSpecific) {
			subjects = new ArrayList<List<TargetMatch>>();
			List<TargetMatch> subject = new ArrayList<TargetMatch>();
			String subjectMatchId = XACMLStrings.FN_GroupContainsKey;
			URI subjectDesignatorType = new URI(StringAttribute.identifier);
			URI subjectDesignatorId = new URI(XACMLStrings.SUBJECT_ID);
			AttributeDesignator subjectDesignator = new AttributeDesignator(
					AttributeDesignator.SUBJECT_TARGET, subjectDesignatorType,
					subjectDesignatorId, false);
			subjectDesignator.setSubjectCategory(new URI(
					XACMLStrings.CATEGORY_ACCESS_SUBJECT));

			StringAttribute subjectValue = new StringAttribute(this.groupName);

			subject.add(createTargetMatch(TargetMatch.SUBJECT, subjectMatchId,
					subjectDesignator, subjectValue));
			subjects.add(subject);
		}

		// create the Resource section
		List<TargetMatch> resource = new ArrayList<TargetMatch>();
		String resourceMatchId = EqualFunction.NAME_STRING_EQUAL;
		URI resourceDesignatorType = new URI(StringAttribute.identifier);
		URI resourceDesignatorId = new URI(XACMLStrings.RESOURCE_ID);
		AttributeDesignator resourceDesignator = new AttributeDesignator(
				AttributeDesignator.RESOURCE_TARGET, resourceDesignatorType,
				resourceDesignatorId, false);

		StringAttribute resourceValue = new StringAttribute(
				KeyFactory.keyToString(requestedResource.getKey()));

		resource.add(createTargetMatch(TargetMatch.RESOURCE, resourceMatchId,
				resourceDesignator, resourceValue));

		// put the Subject and Resource sections into their lists
		resources.add(resource);

		// create & return the new Target
		return new Target(subjects, resources, null);
	}

	/**
	 * Creates the Rule used in the Policy.
	 * 
	 * @return the rule
	 * 
	 * @throws URISyntaxException
	 *             if there is a problem with any of the URIs
	 */
	protected Rule createRule() throws Exception {
		// define the identifier for the rule
		URI ruleId = new URI("AccessRule");

		// define the effect for the Rule
		int effect = Result.DECISION_PERMIT;

		// get the Condition for the rule
		Apply condition;
		try {
			condition = getRuleCondition();
			return new Rule(ruleId, effect, null, null, condition);
		} catch (UnknownIdentifierException e) {
			logger.log(Level.SEVERE,
					"UnknownIdentifierException: " + e.getMessage());
		} catch (FunctionTypeException e) {
			logger.log(Level.SEVERE, "FunctionTypeException: " + e.getMessage());
		}

		return null;
	}

	protected Apply getRuleCondition() throws Exception{		
		
		FunctionFactory factory = FunctionFactory.getGeneralInstance();

		// create the list of functions that need to be conjuncted
		List<Apply> andedFunctions = new ArrayList<Apply>();
		Function function = null;
		List<Evaluatable> functionArgs = null;

		// if it is for all friends, check the friend condition
		// get the functions that the condition uses
		if (this.preference.getAccessType() == AbstractPrivacyPreference.ISALL) {
			function = factory
					.createFunction(XACMLStrings.FN_SubjectIsFriendOfResource);
			functionArgs = new ArrayList<Evaluatable>();
			andedFunctions.add(new Apply(function, functionArgs, false));
		} else if (this.preference.getAccessType() == AbstractPrivacyPreference.ISGROUP) {
			// if it is for a specific group, check group condition
			function = factory.createFunction(XACMLStrings.FN_GroupContainsKey);			
			functionArgs = new ArrayList<Evaluatable>();
			functionArgs.add(new StringAttribute(this.preference.getUserName()
					+ "_group_" + this.preference.getAccessSubject()));
			andedFunctions.add(new Apply(function, functionArgs, false));
		} else {
			// if it is for a specific user, check user name condition
			function = factory.createFunction(EqualFunction.NAME_STRING_EQUAL);
			functionArgs = new ArrayList<Evaluatable>();
			andedFunctions.add(new Apply(function, functionArgs, false));
		}
		
		List<Apply> allConditions = this.createRuleConditions(this.preference, andedFunctions, factory);
		if (allConditions == null)
			allConditions = andedFunctions;
		
		 factory = FunctionFactory.getConditionInstance();      
	     Function andFunction = factory.createFunction(LogicalFunction.NAME_AND);
	      
		return new Apply(andFunction, allConditions, true);
	}

	//to be extended
	public List<Apply> createRuleConditions(
			AbstractPrivacyPreference pref, List<Apply> andedFunctions,
			FunctionFactory factory) throws Exception{
		return null;
	}

	//to be extended
	protected Set<Obligation> createObligations() throws Exception{
		return new HashSet<Obligation>();
	}

	public Policy getPolicy() throws XACMLException {
		// define the identifier for the policy
		URI policyId;
		try {
			policyId = new URI(policyIdString);
		} catch (URISyntaxException e) {
			logger.log(Level.SEVERE, "Could not generate policy ID for policy.");
			logger.log(Level.SEVERE, e.getMessage());
			throw new XACMLException(e);
		}

		// get the combining algorithm for the policy
		URI combiningAlgId;
		try {
			combiningAlgId = new URI(OrderedDenyOverridesRuleAlg.algId);
			CombiningAlgFactory factory = CombiningAlgFactory.getInstance();
			RuleCombiningAlgorithm combiningAlg = (RuleCombiningAlgorithm) (factory
					.createAlgorithm(combiningAlgId));

			// create the target for the policy
			Target policyTarget = createPolicyTarget();

			// create the access rule
			Rule accessRule = createRule();

			// create a list for the rules and add our rules in order
			List<Rule> ruleList = new ArrayList<Rule>();
			ruleList.add(accessRule);

			// create obligations			
			Set<Obligation> obligations = createObligations();
			obligations = this.enhanceObligations(obligations);
			
			// create the policy
			Policy policy = new Policy(policyId, combiningAlg, null,
					policyTarget, null, ruleList, obligations);			
			return policy;
		} catch (Exception e) {
			e.printStackTrace();
			logger.log(Level.SEVERE, e.getMessage());
			throw new XACMLException(e);
		}
	}

	// if the policy specifies that only the latest data is returned (location
	// service)
	// then we add the IsLatestDataOnly attribute to the obligations, before
	// returning
	// the policies
	private Set<Obligation> enhanceObligations(Set<Obligation> obligations)
			throws Exception {
		if (!this.preference.isLatestDataOnly())
			return obligations;
		if (obligations == null) {
			obligations = new HashSet<Obligation>();
			this.addDefaultObligation(obligations);
			return obligations;
		}

		Iterator<Obligation> it = obligations.iterator();
		Obligation ob;
		Set<Obligation> returnedObs = new HashSet<Obligation>();

		while (it.hasNext()) {
			ob = it.next();
			List assignments = new ArrayList();
			for (Object o : ob.getAssignments()) {
				assignments.add(o);
			}
			assignments.add(new Attribute(new URI(XACMLStrings.ATTR_GetLatest),
					null, null, BooleanAttribute.getInstance(true)));
			Obligation newOb = new Obligation(ob.getId(), ob.getFulfillOn(),
					assignments);
			returnedObs.add(newOb);
		}
		return returnedObs;
	}

	// When there's no obligations in the preference, add the default one
	// that indicates if it's getting the latest or all attributes
	private void addDefaultObligation(Set<Obligation> obligations)
			throws Exception {

		List<Attribute> atts = new ArrayList<Attribute>();
		atts.add(new Attribute(new URI(XACMLStrings.ATTR_GetLatest), null,
				null, BooleanAttribute.getInstance(true)));

		Obligation defaultObligation = new Obligation(new URI(
				IDs.ObligationID.DefaultObligation.toString()),
				Result.DECISION_PERMIT, atts);
		obligations.add(defaultObligation);
	}

	@Override
	public String toString() {
		Utilities.setUpFactory();

		ByteArrayOutputStream os = new ByteArrayOutputStream();

		try {
			getPolicy().encode(os, new Indenter());
			return os.toString("UTF-8");
		} catch (Exception e) {
			logger.log(Level.WARNING,
					"Error converting policy to string representation. Exception: "
							+ e.getMessage());
		}

		return null;
	}
}
